package com.zx.cloud.util;

import static java.lang.String.format;
import static java.util.Collections.sort;
import static java.util.Collections.synchronizedMap;
import static java.util.Collections.unmodifiableList;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public final class ReflectionUtils {

	private static final Map<ClassAnnotationCacheKey, List<?>> FIELDS_WITH_ANNOTATION_CACHE = synchronizedMap(new HashMap<ClassAnnotationCacheKey, List<?>>());
	private static final Map<ClassAnnotationCacheKey, List<Method>> METHODS_WITH_ANNOTATION_CACHE = synchronizedMap(new HashMap<ClassAnnotationCacheKey, List<Method>>());

	private ReflectionUtils() {
	}

	public static boolean isPrimitive(Object object) {
		if (object == null) {
			return false;
		}
		Class<?> type = object.getClass();
		return object instanceof String || (object instanceof Integer || Integer.TYPE.equals(type)) || (object instanceof Boolean || Boolean.TYPE.equals(type))
				|| (object instanceof Long || Long.TYPE.equals(type)) || (object instanceof Double || Double.TYPE.equals(type))
				|| (object instanceof Float || Float.TYPE.equals(type)) || (object instanceof Byte || Byte.TYPE.equals(type))
				|| (object instanceof Short || Short.TYPE.equals(type)) || (object instanceof Character || Character.TYPE.equals(type));
	}

	public static <T extends Annotation> List<FieldWithAnnotation<T>> findFieldsWithAnnotation(Class<?> type, Class<T> annotationType) {
		ClassAnnotationCacheKey cacheKey = new ClassAnnotationCacheKey(type, annotationType);
		@SuppressWarnings("unchecked")
		List<FieldWithAnnotation<T>> cachedResults = (List<FieldWithAnnotation<T>>) FIELDS_WITH_ANNOTATION_CACHE.get(cacheKey);
		if (cachedResults != null)
			return cachedResults;
		List<FieldWithAnnotation<T>> fieldsWithAnnotation = new ArrayList<FieldWithAnnotation<T>>();
		while (!Object.class.equals(type)) {
			for (Field field : type.getDeclaredFields()) {
				T annotation = field.getAnnotation(annotationType);
				if (annotation != null)
					fieldsWithAnnotation.add(new FieldWithAnnotation<T>(field, annotation));
			}
			type = type.getSuperclass();
		}
		fieldsWithAnnotation = unmodifiableList(fieldsWithAnnotation);
		FIELDS_WITH_ANNOTATION_CACHE.put(cacheKey, fieldsWithAnnotation);
		return fieldsWithAnnotation;
	}

	public static <T extends Annotation> List<Method> findMethodsWithAnnotation(Class<?> type, Class<T> annotationType) {
		ClassAnnotationCacheKey cacheKey = new ClassAnnotationCacheKey(type, annotationType);
		List<Method> cachedResults = METHODS_WITH_ANNOTATION_CACHE.get(cacheKey);
		if (cachedResults != null)
			return cachedResults;
		List<Method> methodsWithAnnotation = new ArrayList<Method>();
		while (!Object.class.equals(type)) {
			for (Method method : type.getDeclaredMethods()) {
				T annotation = method.getAnnotation(annotationType);
				if (annotation != null)
					methodsWithAnnotation.add(method);
			}
			type = type.getSuperclass();
		}
		methodsWithAnnotation = unmodifiableList(methodsWithAnnotation);
		METHODS_WITH_ANNOTATION_CACHE.put(cacheKey, methodsWithAnnotation);
		return methodsWithAnnotation;
	}

	public static Class<?> getFirstParameterizedTypeArgument(Field field) {
		Type type = field.getGenericType();
		if (!(type instanceof ParameterizedType))
			return null;
		ParameterizedType parameterizedType = (ParameterizedType) type;
		Type firstTypeArgument = parameterizedType.getActualTypeArguments()[0];
		return (firstTypeArgument instanceof Class) ? (Class<?>) firstTypeArgument : null;
	}

	public static List<Method> getAccessors(Class<?> clazz) {
		if (clazz == null)
			throw new IllegalArgumentException("The 'clazz' parameter cannot be null.");
		List<Method> methods = new ArrayList<Method>();
		for (Method method : clazz.getMethods()) {
			String methodName = method.getName();
			if (!"getClass".equals(methodName)
					&& !"hashCode".equals(methodName)
					&& method.getReturnType() != null
					&& !Void.class.equals(method.getReturnType())
					&& method.getParameterTypes().length == 0
					&& ((methodName.startsWith("get") && methodName.length() > 3) || (methodName.startsWith("is") && methodName.length() > 2) || (methodName
							.startsWith("has") && methodName.length() > 3)))
				methods.add(method);
		}
		sort(methods, new Comparator<Method>() {
			@Override
			public int compare(Method method1, Method method2) {
				return method1.getName().compareTo(method2.getName());
			}
		});
		return unmodifiableList(methods);
	}

	public static String toString(Object object) {
		StringBuilder buffer = new StringBuilder(object.getClass().getSimpleName());
		buffer.append("[");
		boolean first = true;
		for (Method method : getAccessors(object.getClass())) {
			if (first)
				first = false;
			else
				buffer.append(" ");
			try {
				String methodName = method.getName();
				int offset = methodName.startsWith("is") ? 2 : 3;
				methodName = methodName.substring(offset, offset + 1).toLowerCase() + methodName.substring(offset + 1);
				buffer.append(methodName);
				buffer.append("=");
				buffer.append(method.invoke(object));
			} catch (Exception e) {
				throw new IllegalStateException("Unable to reflectively invoke " + method + " on " + object.getClass(), e);
			}
		}
		buffer.append("]");
		return buffer.toString();
	}

	public static int hashCode(Object object) {
		if (object == null)
			return 0;
		int hashCode = 17;
		for (Method method : getAccessors(object.getClass())) {
			try {
				Object result = method.invoke(object);
				if (result != null)
					hashCode = hashCode * 31 + result.hashCode();
			} catch (Exception e) {
				throw new IllegalStateException("Unable to reflectively invoke " + method + " on " + object, e);
			}
		}
		return hashCode;
	}

	public static boolean equals(Object object1, Object object2) {
		if (object1 == null && object2 == null)
			return true;
		if (!(object1 != null && object2 != null))
			return false;
		if (!(object1.getClass().isInstance(object2) || object2.getClass().isInstance(object1)))
			return false;
		Set<Method> accessorMethodsIntersection = new HashSet<Method>(getAccessors(object1.getClass()));
		accessorMethodsIntersection.retainAll(getAccessors(object2.getClass()));
		for (Method method : accessorMethodsIntersection) {
			try {
				Object result1 = method.invoke(object1);
				Object result2 = method.invoke(object2);
				if (result1 == null && result2 == null)
					continue;
				if (!(result1 != null && result2 != null))
					return false;
				if (!result1.equals(result2))
					return false;
			} catch (Exception e) {
				throw new IllegalStateException("Unable to reflectively invoke " + method, e);
			}
		}
		return true;
	}

	public static class FieldWithAnnotation<T extends Annotation> {

		private Field field;
		private T annotation;

		public FieldWithAnnotation(Field field, T annotation) {
			this.field = field;
			this.annotation = annotation;
		}

		public Field getField() {
			return field;
		}

		public T getAnnotation() {
			return annotation;
		}

		@Override
		public String toString() {
			return format("Field %s.%s (%s): %s", field.getDeclaringClass().getName(), field.getName(), field.getType(), annotation);
		}
	}

	private static final class ClassAnnotationCacheKey {

		private final Class<?> clazz;
		private final Class<? extends Annotation> annotation;

		private ClassAnnotationCacheKey(Class<?> clazz, Class<? extends Annotation> annotation) {
			this.clazz = clazz;
			this.annotation = annotation;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + (annotation == null ? 0 : annotation.hashCode());
			result = prime * result + (clazz == null ? 0 : clazz.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ClassAnnotationCacheKey other = (ClassAnnotationCacheKey) obj;
			if (annotation == null) {
				if (other.annotation != null)
					return false;
			} else if (!annotation.equals(other.annotation)) {
				return false;
			}
			if (clazz == null) {
				if (other.clazz != null)
					return false;
			} else if (!clazz.equals(other.clazz)) {
				return false;
			}
			return true;
		}
	}
}